"""
This module converts requested URLs to callback view functions.

RegexURLResolver is the main class here. Its resolve() method takes a URL (as
a string) and returns a tuple in this format:

    (view_function, function_args, function_kwargs)
"""

from django.http import Http404
from django.core.exceptions import ImproperlyConfigured, ViewDoesNotExist
from django.utils.encoding import iri_to_uri, force_unicode, smart_str
from django.utils.functional import memoize
import re

try:
    reversed
except NameError:
    from django.utils.itercompat import reversed     # Python 2.3 fallback

_resolver_cache = {} # Maps urlconf modules to RegexURLResolver instances.
_callable_cache = {} # Maps view and url pattern names to their view functions.

class Resolver404(Http404):
    pass

class NoReverseMatch(Exception):
    # Don't make this raise an error when used in a template.
    silent_variable_failure = True

def get_callable(lookup_view, can_fail=False):
    """
    Convert a string version of a function name to the callable object.

    If the lookup_view is not an import path, it is assumed to be a URL pattern
    label and the original string is returned.

    If can_fail is True, lookup_view might be a URL pattern label, so errors
    during the import fail and the string is returned.
    """
    if not callable(lookup_view):
        try:
            # Bail early for non-ASCII strings (they can't be functions).
            lookup_view = lookup_view.encode('ascii')
            mod_name, func_name = get_mod_func(lookup_view)
            if func_name != '':
                lookup_view = getattr(__import__(mod_name, {}, {}, ['']), func_name)
        except (ImportError, AttributeError):
            if not can_fail:
                raise
        except UnicodeEncodeError:
            pass
    return lookup_view
get_callable = memoize(get_callable, _callable_cache, 1)

def get_resolver(urlconf):
    if urlconf is None:
        from django.conf import settings
        urlconf = settings.ROOT_URLCONF
    return RegexURLResolver(r'^/', urlconf)
get_resolver = memoize(get_resolver, _resolver_cache, 1)

def get_mod_func(callback):
    # Converts 'django.views.news.stories.story_detail' to
    # ['django.views.news.stories', 'story_detail']
    try:
        dot = callback.rindex('.')
    except ValueError:
        return callback, ''
    return callback[:dot], callback[dot+1:]

def reverse_helper(regex, *args, **kwargs):
    """
    Does a "reverse" lookup -- returns the URL for the given args/kwargs.
    The args/kwargs are applied to the given compiled regular expression.
    For example:

        >>> reverse_helper(re.compile('^places/(\d+)/$'), 3)
        'places/3/'
        >>> reverse_helper(re.compile('^places/(?P<id>\d+)/$'), id=3)
        'places/3/'
        >>> reverse_helper(re.compile('^people/(?P<state>\w\w)/(\w+)/$'), 'adrian', state='il')
        'people/il/adrian/'

    Raises NoReverseMatch if the args/kwargs aren't valid for the regex.
    """
    # TODO: Handle nested parenthesis in the following regex.
    result = re.sub(r'\(([^)]+)\)', MatchChecker(args, kwargs), regex.pattern)
    return result.replace('^', '').replace('$', '').replace('\\', '')

class MatchChecker(object):
    "Class used in reverse RegexURLPattern lookup."
    def __init__(self, args, kwargs):
        self.args, self.kwargs = args, kwargs
        self.current_arg = 0

    def __call__(self, match_obj):
        # match_obj.group(1) is the contents of the parenthesis.
        # First we need to figure out whether it's a named or unnamed group.
        #
        grouped = match_obj.group(1)
        m = re.search(r'^\?P<(\w+)>(.*?)$', grouped, re.UNICODE)
        if m: # If this was a named group...
            # m.group(1) is the name of the group
            # m.group(2) is the regex.
            try:
                value = self.kwargs[m.group(1)]
            except KeyError:
                # It was a named group, but the arg was passed in as a
                # positional arg or not at all.
                try:
                    value = self.args[self.current_arg]
                    self.current_arg += 1
                except IndexError:
                    # The arg wasn't passed in.
                    raise NoReverseMatch('Not enough positional arguments passed in')
            test_regex = m.group(2)
        else: # Otherwise, this was a positional (unnamed) group.
            try:
                value = self.args[self.current_arg]
                self.current_arg += 1
            except IndexError:
                # The arg wasn't passed in.
                raise NoReverseMatch('Not enough positional arguments passed in')
            test_regex = grouped
        # Note we're using re.match here on purpose because the start of
        # to string needs to match.
        if not re.match(test_regex + '$', force_unicode(value), re.UNICODE):
            raise NoReverseMatch("Value %r didn't match regular expression %r" % (value, test_regex))
        return force_unicode(value)

class RegexURLPattern(object):
    def __init__(self, regex, callback, default_args=None, name=None):
        """
        regex is a string representing a regular expression.
        callback is either a string like 'foo.views.news.stories.story_detail'
        which represents the path to a module and a view function name, or a
        callable object (view).
        """
        self.regex = re.compile(regex, re.UNICODE)
        if callable(callback):
            self._callback = callback
        else:
            self._callback = None
            self._callback_str = callback
        self.default_args = default_args or {}
        self.name = name

    def __repr__(self):
        return '<%s %s %s>' % (self.__class__.__name__, self.name, self.regex.pattern)

    def add_prefix(self, prefix):
        """
        Adds the prefix string to a string-based callback.
        """
        if not prefix or not hasattr(self, '_callback_str'):
            return
        self._callback_str = prefix + '.' + self._callback_str

    def resolve(self, path):
        """
        Attempts to match the path against this pattern.

        If the pattern matches, returns a tuple of the function to
        handle the request, the arguments to pass, and the keyword
        arguments to pass, including the extra keyword arguments given
        in the constructor.

        If the pattern does not match, returns None.
        """
        match = self.regex.search(path)
        if not match:
            return None

        # If there are any named groups, use those as kwargs, ignoring
        # non-named groups. Otherwise, pass all non-named arguments as
        # positional arguments.
        kwargs = match.groupdict()
        if kwargs:
            args = ()
        else:
            args = match.groups()
        # In both cases, pass any extra_kwargs as **kwargs.
        kwargs.update(self.default_args)

        return self.callback, args, kwargs

    def _get_callback(self):
        if self._callback is not None:
            return self._callback
        try:
            self._callback = get_callable(self._callback_str)
        except ImportError, e:
            mod_name, _ = get_mod_func(self._callback_str)
            raise ViewDoesNotExist, "Could not import %s. Error was: %s" % (mod_name, str(e))
        except AttributeError, e:
            mod_name, func_name = get_mod_func(self._callback_str)
            raise ViewDoesNotExist, "Tried %s in module %s. Error was: %s" % (func_name, mod_name, str(e))
        return self._callback
    callback = property(_get_callback)

    def reverse(self, viewname, *args, **kwargs):
        mod_name, func_name = get_mod_func(viewname)
        try:
            lookup_view = getattr(__import__(mod_name, {}, {}, ['']), func_name)
        except (ImportError, AttributeError):
            raise NoReverseMatch
        if lookup_view != self.callback:
            raise NoReverseMatch
        return self.reverse_helper(*args, **kwargs)

    def reverse_helper(self, *args, **kwargs):
        return reverse_helper(self.regex, *args, **kwargs)


class BaseRegexURLResolver(object):
    def __init__(self, regex, url_patterns, default_kwargs=None):
        """
        regex is a string representing a regular expression.
        url_patterns is a list of URL patterns to check.

        If url_patterns is a callable, it is called before being used.
        """
        self.regex = re.compile(regex, re.UNICODE)
        self.callback = None # FIXME: why does this class have 'callback'?
        self._lazy_url_patterns = url_patterns
        self.default_kwargs = default_kwargs or {}

    # Ensures that url_patterns is called exactly once before being
    # used, if it is callable.
    def _get_url_patterns(self):
        if not hasattr(self, '_url_patterns'):
            if callable(self._lazy_url_patterns):
                self._url_patterns = self._lazy_url_patterns()
            else:
                self._url_patterns = self._lazy_url_patterns

        return self._url_patterns
    url_patterns = property(_get_url_patterns)

    def _get_reverse_dict(self):
        if not hasattr(self, '_reverse_dict'):
            self._reverse_dict = {}
            def set_reverse_entry(key, value):
                if key is None:
                    return
                if key in self._reverse_dict and self._reverse_dict[key] != value:
                    import warnings
                    warnings.warn('Duplicate named URL specified: %s resolves to both %s and %s.' % (key, value, self._reverse_dict[key]))
                self._reverse_dict[key] = value

            for pattern in reversed(self.url_patterns):
                if isinstance(pattern, BaseRegexURLResolver):
                    for key, value in pattern.reverse_dict.iteritems():
                        set_reverse_entry(key, (pattern,) + value)
                else:
                    set_reverse_entry(pattern.callback, (pattern,))
                    set_reverse_entry(pattern.name, (pattern,))
        return self._reverse_dict
    reverse_dict = property(_get_reverse_dict)
    
    def resolve(self, path):
        tried = []
        # Use 'search' because only the beginning should match.
        # FIXME: Maybe the regex should have '.*' appended, then use
        # regex.match.
        match = self.regex.search(path)
        if not match:
            return None
        
        new_path = path[match.end():]
        for pattern in self.url_patterns:
            try:
                sub_match = pattern.resolve(new_path)
            except Resolver404, e:
                tried.extend([(pattern.regex.pattern + '   ' + t) for t in e.args[0]['tried']])
            else:
                if sub_match:
                    callback, args, kwargs = sub_match
                    # The ordering matters here: kwargs from the sub
                    # match override kwargs from our default_kwargs,
                    # which in turn override any matches from this
                    # resolver's regex.
                    sub_match_dict = dict([(smart_str(k), v) for k, v in match.groupdict().iteritems()]) # Not using a generator expression, for Python 2.3.
                    sub_match_dict.update(self.default_kwargs)
                    for k, v in kwargs.iteritems():
                        sub_match_dict[smart_str(k)] = v
                    return callback, args, sub_match_dict
                tried.append(pattern.regex.pattern)
        raise Resolver404, {'tried': tried, 'path': new_path}

    def reverse(self, lookup_view, *args, **kwargs):
        """
        Return the URL corresponding to lookup_view (either a view name or the
        name of a URL pattern) called with the given arguments.
        """
        try:
            lookup_view = get_callable(lookup_view, True)
        except (ImportError, AttributeError):
            raise NoReverseMatch
        if lookup_view in self.reverse_dict:
            return u''.join([reverse_helper(part.regex, *args, **kwargs) for part in self.reverse_dict[lookup_view]])
        raise NoReverseMatch

    
class RegexURLResolver(BaseRegexURLResolver):
    def __init__(self, regex, urlconf_name, default_kwargs=None):
        # regex is a string representing a regular expression.
        # urlconf_name is a string representing the module containing urlconfs.
        self.urlconf_name = urlconf_name
        super(RegexURLResolver, self).__init__(regex, self._get_url_patterns, default_kwargs)

    def __repr__(self):
        return '<%s %s %s>' % (self.__class__.__name__, self.urlconf_name, self.regex.pattern)


    def _get_urlconf_module(self):
        try:
            return self._urlconf_module
        except AttributeError:
            try:
                self._urlconf_module = __import__(self.urlconf_name, {}, {}, [''])
            except Exception, e:
                # Either an invalid urlconf_name, such as "foo.bar.", or some
                # kind of problem during the actual import.
                raise ImproperlyConfigured, "Error while importing URLconf %r: %s" % (self.urlconf_name, e)
            return self._urlconf_module
    urlconf_module = property(_get_urlconf_module)

    # FIXME: should this fail softly if the module has no urlpatterns?
    # There used to be a check for this in _get_reverse_dict, but nowhere else.
    def _get_url_patterns(self):
        return self.urlconf_module.urlpatterns
    url_patterns = property(_get_url_patterns)

    def _resolve_special(self, view_type):
        callback = getattr(self.urlconf_module, 'handler%s' % view_type)
        mod_name, func_name = get_mod_func(callback)
        try:
            return getattr(__import__(mod_name, {}, {}, ['']), func_name), {}
        except (ImportError, AttributeError), e:
            raise ViewDoesNotExist, "Tried %s. Error was: %s" % (callback, str(e))

    def resolve404(self):
        return self._resolve_special('404')

    def resolve500(self):
        return self._resolve_special('500')

    # FIXME: this seems to be unused.
    def reverse_helper(self, lookup_view, *args, **kwargs):
        sub_match = self.reverse(lookup_view, *args, **kwargs)
        result = reverse_helper(self.regex, *args, **kwargs)
        return result + sub_match

def resolve(path, urlconf=None):
    return get_resolver(urlconf).resolve(path)

def reverse(viewname, urlconf=None, args=None, kwargs=None):
    args = args or []
    kwargs = kwargs or {}
    return iri_to_uri(u'/' + get_resolver(urlconf).reverse(viewname, *args, **kwargs))

